package ms.jasim.framework;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import ms.jasim.model.Actor;
import ms.jasim.model.Capability;
import ms.jasim.model.Goal;
import ms.jasim.model.GoalRequest;
import ms.jasim.model.JasimModel;
import ms.jasim.model.items.Item;
import ms.jasim.model.items.PropertyDescriptor;
import ms.jasim.model.items.PropertyList;
import ms.jasim.pddl.PddlPredicate;
import ms.jasim.pddl.PddlSolution;
import ms.jasim.pddl.PddlSolution.Functor;
import ms.jasim.pddl.PddlSolution.PddlAction;
import ms.utils.EventListImpl;
import ms.utils.IEvent;
import ms.utils.INamedItem;
import ms.utils.NamedList;
import ms.utils.PerformanceTimer;

import org.eclipse.emf.ecore.util.EcoreUtil;

public class SolutionSimulator {

	public static class SimulatorArg {
		private boolean cancel = false;
		private int time;
		private SolutionSimulator simulator;

		public int getTime() {
			return time;
		}

		public void setCancel(boolean cancel) {
			this.cancel = cancel;
		}

		public boolean isCancel() {
			return cancel;
		}

		void setTime(int time) {
			this.time = time;
		}

		public SolutionSimulator getSimulator() {
			return simulator;
		}

		public void setSimulator(SolutionSimulator simulator) {
			this.simulator = simulator;
		}
	}

	public class Agent implements INamedItem {
		private String name;
		//private int availableBudget = Integer.MAX_VALUE;
		private int requiredBudget = 0;

		public final ArrayList<SimulatedAction> ActionQueue = new ArrayList<SimulatedAction>();
		private Map<Goal, PddlAction> plan = new HashMap<Goal, PddlAction>();

		private Actor actor;
		private Item budgetItem;

		public Agent(String name) {
			this.name = name;
			actor = model.getActors().get(name);
			if (actor != null) {
				PropertyList propertyList = actor.getProperties();
				budgetItem = propertyList.get(PddlPredicate.ACTOR_BUDGET);
				if (budgetItem == null) {
					Item resolveProperty = actor.resolveProperty(actorBudgetDescriptor);
					if (resolveProperty != null) {
						budgetItem = (Item) EcoreUtil.copy(resolveProperty);
						propertyList.getItem().add(budgetItem);
					}
				}
			}
		}

		public Actor getActor() {
			if (actor == null)
				actor = model.getActors().get(name); 
			return actor;
		}
		
		public String getName() {
			return name;
		}

		public void addGoal(Goal... goals) {
			for (Goal g : goals) {
				PddlAction action = plan.get(g);
				if (action != null) {
					SimulatedAction item = new SimulatedAction(this, g, action);
					requiredBudget += item.getBudget();
					ActionQueue.add(item);
					plan.remove(g); // remove this action to avoid looping
				}
			}
		}

		public void addPlan(PddlAction act) {
			Goal goal = model.getGoals().get(act.getArgument((act.getFunctor() == Functor.PASSES ? 2 : 1)));
			if (goal != null)
				plan.put(goal, act);
		}

		public SimulatedAction getActiveAction() {
			SimulatedAction result = null;
			for (int i = 0; i < ActionQueue.size() && result == null; i++)
				if (ActionQueue.get(i).isReady()) {
					result = ActionQueue.get(i);
					if (i > 0) { // move the active action to the first position, if it's not
						ActionQueue.remove(i);
						ActionQueue.add(0, result);
					}
				}
			return result;
		}

		public boolean isBusy() {
			for (SimulatedAction act : ActionQueue)
				if (act.isExecuting())
					return true;
			return false;
		}
		
		public SimulatedAction getExecutingAction() {
			for (SimulatedAction act : ActionQueue)
				if (act.isExecuting())
					return act;
			return null;
		}

		public SimulatedAction getSimulatingAction(Goal goal) {
			for (SimulatedAction act : ActionQueue) {
				if (act.goal == goal)
					return act;
			}
			return null;
		}
		
		public void setAvailableBudget(int availableBudget) {
			if (budgetItem != null)
				budgetItem.setValue(null, availableBudget);
		}
		
		public int getAvailableBudget() {
			return budgetItem != null ? (Integer) budgetItem.getValue(null) : Integer.MAX_VALUE;
		}

		public int getRequireBudget() {
			return requiredBudget;
		}
		
		public boolean isInconsistence() {
			int budget = getAvailableBudget();
			int numIns = Integer.MAX_VALUE;
			if (requiredBudget > budget) {
				System.out.printf("Agent=%s, budget=%d, requires=%d\r\n", getName(), budget, requiredBudget);
			}
			else if (actorInstance != null) {
				numIns = (Integer)actor.resolveProperty(actorInstance).getValue(null);
			}
			return requiredBudget > budget || numIns <= 0;
		}
		
		public void actionFinished(SimulatedAction act) {
			requiredBudget -= act.getBudget();
			setAvailableBudget(getAvailableBudget() - act.getBudget());
		}
		
	}

	public class SimulatedAction {
		private Goal goal;
		private PddlAction action;
		private Agent agent;
		private int start, end, remainDuration, budget;

		public SimulatedAction(Agent agent, Goal goal, PddlAction action) {
			this.goal = goal;
			this.action = action;
			this.agent = agent;
			start = end = -1;
			budget = 1;
			
			writeMessage("CREATE ACTION: agent=%s, goal=%s, action=%s\r\n", agent.getName(), goal.getName(), action.getFunctor().toString());
			if (action.getFunctor() == Functor.SATISFIES) {
				Actor act = model.getActors().get(action.getArgument(0));
				Capability cap = act.findCapability(goal);
				if (cap != null) {
					remainDuration = timeEffortDescriptor != null ? 
										(Integer)cap.resolveProperty(timeEffortDescriptor).getValue(null) : 1;
					budget = workEffortDescriptor != null ?
								(Integer)cap.resolveProperty(workEffortDescriptor).getValue(null) : 1;
				}
			} else 
				remainDuration = 3;
		}

		public int getPriority() {
			return 1;
		}

		public Agent getAgent() {
			return agent;
		}
		
		public Agent getAgent(int index) {
			Agent result = null;
			result = agents.get(action.getArgument(index));
			return result;
		}

		public Goal getGoal() {
			return goal;
		}
		
		public Goal getGoal(int index) {
			Goal result = null;
			result = model.getGoals().get(action.getArgument(index));
			return result;
		}

		public PddlAction getAssociateAction() {
			return action;
		}

		public void execute(int timeCircle) {
			Agent ag;
			Goal g1, g2, g3;

			if (start == -1) {
				start = timeCircle;
			}
			remainDuration--;
			if (remainDuration <= 0) {
				switch (action.getFunctor()) {
				case AND_DECOMPOSES2:
					g1 = model.getGoals().get(action.getArgument(2));
					g2 = model.getGoals().get(action.getArgument(3));
					agent.addGoal(g1, g2);
					break;
				case AND_DECOMPOSES3:
					g1 = model.getGoals().get(action.getArgument(2));
					g2 = model.getGoals().get(action.getArgument(3));
					g3 = model.getGoals().get(action.getArgument(4));
					agent.addGoal(g1, g2, g3);
					break;
				case PASSES:
					ag = agents.get(action.getArgument(1));
					g1 = model.getGoals().get(action.getArgument(2));
					if (ag != null && g1 != null)
						ag.addGoal(g1);
					else 
						System.out.println("Action corrupted: " + action.toString());
					break;
				case SATISFIES:
					satisfiedGoals.add(goal);
				}
			
				end = timeCircle;
				agent.ActionQueue.remove(this);
				agent.actionFinished(this);
				finishedActions.add(action);
			}
		}

		public int getStart() {
			return start;
		}

		public int getEnd() {
			return end;
		}

		public int getBudget() {
			return budget;
		}
		
		public boolean isReady() {
			boolean result = false;
			switch(action.getFunctor()) {
			case SATISFIES:
				//Agent ag = agents.get(action.getArgument(0));
				//if (!ag.isBusy())
				result = isExecuting() || !agent.isBusy();
				for (Goal req: goal.getRequires())
					if (!(result = satisfiedGoals.indexOf(req) >= 0)) 
						break;
				break;
			default:
				result = true;
			}
			return result;
		}

		public boolean isExecuting() {
			return remainDuration > 0 && start >= 0;
		}

		@Override
		public String toString() {
			return action.toString();
		}

	}

	public class TimeSlot {
		public final int time;
		public final ArrayList<SimulatedAction> actions = new ArrayList<SimulatedAction>();

		public TimeSlot(int time) {
			this.time = time;
		}
	}

	private JasimModel model;
	private IJasimAppContext context;
	private IMessageConsole stdout;
	private PddlSolution solution;
	private EventListImpl<SimulatorArg> listener = new EventListImpl<SimulatorArg>();

	public final NamedList<Agent> agents = new NamedList<Agent>();
	public final ArrayList<TimeSlot> timeSlots = new ArrayList<TimeSlot>();
	
	private boolean completed;
	private ArrayList<PddlAction> finishedActions;
	private ArrayList<SimulatedAction> history;
	private ArrayList<Goal> satisfiedGoals;
	
	private int initalTime;
	private int endTime;
	private PropertyDescriptor timeEffortDescriptor;
	private PropertyDescriptor actorBudgetDescriptor;
	private PropertyDescriptor  workEffortDescriptor;
	private PropertyDescriptor actorInstance;

	public SolutionSimulator(IJasimAppContext context, JasimModel model, PddlSolution solution) {
		this.model = model;
		this.context = context;
		this.solution = solution;
		this.finishedActions = new ArrayList<PddlAction>();
		this.history = new ArrayList<SimulatedAction>();
		this.satisfiedGoals = new ArrayList<Goal>();
		
		this.timeEffortDescriptor = model.findPropertyDescriptor(Capability.class, PddlPredicate.TIME_EFFORT);
		this.actorBudgetDescriptor = model.findPropertyDescriptor(Actor.class, PddlPredicate.ACTOR_BUDGET);
		this.workEffortDescriptor = model.findPropertyDescriptor(Capability.class, PddlPredicate.WORK_EFFORT);
		this.actorInstance = model.findPropertyDescriptor(Actor.class, "#Instance");
		
		stdout = this.context.getService(IMessageConsole.class);
		
		PerformanceTimer.createEntry("SOL-SIMUL@Init", String.format("Initialize simulator (%s)", solution.getName()));
		
		for (PddlAction act : solution)
			if (!act.Name().startsWith("COMBINE")) {
				Agent a = resolveAgent(act.getArgument(0));
				a.addPlan(act);
			}

		for (Actor act : model.getActors())
			for (GoalRequest request : act.getRequests())
				if (request.getGoal()!= null)
					resolveAgent(act.getName()).addGoal(request.getGoal());
		
		PerformanceTimer.closeEntry();
	}

	public void addListener(IEvent<SimulatorArg> listener) {
		this.listener.add(listener);
	}

	public void removeListener(IEvent<SimulatorArg> listener) {
		this.listener.remove(listener);
	}

	public int simulate(int initalTime, int totalTime) {
		ArrayList<SimulatedAction> activeActions = new ArrayList<SimulatedAction>();
		timeSlots.clear();
		this.initalTime = initalTime;
		int timeCircle = initalTime;
		
		SimulatorArg arg = new SimulatorArg();
		arg.setSimulator(this);

		PerformanceTimer.createEntry("SOL-SIMUL@simulate", String.format("Simulate solution (%s)", solution.getName()));
		
		for (boolean breakNow = false; timeCircle <= totalTime && !breakNow; timeCircle++) {
			activeActions.clear();
			for (Agent ag : agents) {
				SimulatedAction action = ag.getActiveAction();
				if (action != null) {
					activeActions.add(action);
					if (history.indexOf(action) < 0)
						history.add(action);
				}
			}
			if (activeActions.size() > 0) {
				TimeSlot slot = new TimeSlot(timeCircle);
				timeSlots.add(slot);
				for (SimulatedAction act : activeActions) {
					slot.actions.add(act);
					act.execute(timeCircle);
					
					writeMessage("[%d] %s: %s\n", timeCircle, act.getAgent().getName(), act.toString());
				}
			} else
				breakNow = true;
			
			completed = activeActions.size() == 0;
			
			arg.setTime(timeCircle);
			listener.fire(this, arg);
			
			if (arg.isCancel())
				breakNow = true;
		}
		this.endTime = timeCircle;
		
		PerformanceTimer.closeEntry();
		
		// dump actor buget information
		writeMessage("Actor Budget Information\r\n");
		for(Agent ag: agents)
			writeMessage("%s: inital=%s, require=%d, consume=%d\r\n", ag.getName(), ag.getAvailableBudget(), 
					ag.getRequireBudget(), 0);
		
		return timeCircle;
	}

	public boolean isCompleted() {
		return completed;
	}
	
	private void writeMessage(String template, Object... params) {
		/*if (stdout != null)
			stdout.write("Simulation", String.format(template, params));*/
	}

	private Agent resolveAgent(String name) {
		Agent result = agents.get(name);
		if (result == null) {
			result = new Agent(name);
			agents.add(result);
		}
		return result;
	}

	public Agent getAgent(String name) {
		return agents.get(name);
	}

	public JasimModel getModel() {
		return model;
	}

	public PddlSolution getSolution() {
		return solution;
	}

	public int getInitialTime() {
		return initalTime;
	}

	public int getEndTime() {
		return this.endTime;
	}
	
	public List<PddlAction> getFinishedActions() {
		return finishedActions;
	}

	public List<SimulatedAction> getHistory() {
		return history;
	}

	public static class HistoryEntry {
		public SimulatedAction action;
		public int start, end; 
		
		public HistoryEntry(SimulatedAction action, int end) {
			this.action = action;
			this.start = action.start;
			this.end = action.end != -1 ? action.end :  end;
		}
		
		@Override
		public String toString() {
			// agent, actionIdx, action text, start-time, end-time, tbc|ov
			return String.format("%s, %d, %s, %d, %d, %s", 
					action.getAgent().getName(),
					action.getAssociateAction().index,
					action.getAssociateAction().toString(),
					start, end,
					end == action.getEnd() ? "ov" : "tbc");
		}
	}
	
	public List<HistoryEntry> convertTimeslotToHistory() {
		ArrayList<HistoryEntry> history = new ArrayList<HistoryEntry>();
		//ArrayList<SimulatedAction> newActions = new ArrayList<SimulatedAction>(5);
		Hashtable<SimulatedAction, Boolean> doneAction = new Hashtable<SimulatedAction, Boolean>();
		
		int lastTime = timeSlots.size() > 0 ? timeSlots.get(timeSlots.size() - 1).time : 0;
		
		for (Iterator<TimeSlot> iterator = timeSlots.iterator(); iterator.hasNext();) {
			TimeSlot slot = iterator.next();
			for(SimulatedAction act: slot.actions)
				if (!doneAction.containsKey(act)) {
					history.add(new HistoryEntry(act, lastTime));
					doneAction.put(act, true);
				}
			/*newActions.clear();
			newActions.addAll(slot.actions);
			for (HistoryEntry e: history) 
				if (e.end < 0)
					if (!iterator.hasNext())
						e.end = slot.time; 
					else if (newActions.indexOf(e.action) < 0) 
						e.end = slot.time - 1;
					else
						newActions.remove(e.action);
			if (iterator.hasNext())
				for (SimulatedAction action: newActions)
					history.add(new HistoryEntry(action, slot.time));*/
		}
		return history;
	}
	
	public String convertTimeSlotsToString() {
		StringBuilder builder = new StringBuilder();
		for (HistoryEntry e: convertTimeslotToHistory()) 
			builder.append(e.toString() + "\r\n");
		return builder.toString();
	}

	
	public void copySatisfiedGoals(SolutionSimulator simulator) {
		for(Goal tg: simulator.satisfiedGoals) {
			Goal g = model.getGoals().get(tg.getName());
			if (g != null)
				satisfiedGoals.add(g);
		}
	}
}
